#! /usr/bin/python
'''
使用类重构，修改中
'''

import os
import json
import argparse
import xlwt

NAME_FORMAT_ERROR = r"format error, naming rule: @{organization}/{component_name}."
COMPONENT_NAME_TOO_LONG = r"maximum length is 63."
NAME_NOT_LOWER = r"should be all lowercase letters."

VERSION_ERROR = r"value incorrect, please check."
ONLY_ONE_DESTPATH = r"not list."

COMPONENT_NAME_ERROR = r"should same with 'name' field's component part."
SUBSYSTEM_MUST_LOWER = r"should consist of only lowercase letters."
SYSCAP_HEADER_ERROR = r"should begin with 'SystemCapability.'"
PASCAL_CASING_STYLE = r"every word that the first letter is capitalized."
SYSCAP_FORMAT_ERROR = r"format error. rule: SystemCapability.{Subsystem}.{ComponentCapbility}[.{SubCapbility}] "
FEATURE_SEPARATOR_ERROR = r"use '_' to separate component name and feature name"
FEATURE_PREFIX_ERROR = r"prefix should same with component name."

ERROR_EMPTY = r"can't be empty."
NO_FIELD = r"no such field."
CHECK_RULE_2_1 = r"规则2.1 部件描述文件中字段须准确。"

def is_all_lower(s:str):
    '''
    @func: 判断 s 是否全小写字母。
    @note: 不能包含其他字符。
    '''
    if not s:
        return False
    for i in s:
        if not (97 <= ord(i) <= 122):
            return False
    return True

def format_str_value(s:str):
    '''
    @func: 将 s 字符串的数字部分和单位分离
    '''
    if s[0] == '~': # 去除 ram 前面的 '~' 字符
        s = s[1:]
    if s.isdigit():
        return float(s), ''
    for n, u in enumerate(s):
        if not (u.isdigit() or u == '.' or u == '-'):
            break
    try:
        num = float(s[:n])
    except ValueError as e:
        num = 0
    unit = s[n:]
    return num, unit

def get_all_bundle_json() -> list:
    cmd = 'find -name bundle.json -not -path "./out/*"'
    bundle_josn_list = os.popen(cmd).readlines()
    return bundle_josn_list

def export_to_xls(all_error_json:dict, output_path:str):
    workbook = xlwt.Workbook(encoding='utf-8', style_compression=0)
    worksheet = workbook.add_sheet('bundle.json warnning', cell_overwrite_ok=True)
    # 标题栏
    worksheet.write(0, 0, '子系统')
    worksheet.write(0, 1, '部件')
    worksheet.write(0, 2, '文件')
    worksheet.write(0, 3, '违反规则')
    worksheet.write(0, 4, '详细')
    worksheet.write(0, 5, '说明')
    # 设置列宽
    worksheet.col(0).width=256*20
    worksheet.col(1).width=256*26
    worksheet.col(2).width=256*50
    worksheet.col(3).width=256*38
    worksheet.col(4).width=256*26
    worksheet.col(5).width=256*50
    
    raw_num = 1
    for subsystem in all_error_json:
        # worksheet.write(raw_num, 0, subsystem)
        for component in all_error_json[subsystem]:
            # worksheet.write(raw_num, 1, component)
            for bundle_path in all_error_json[subsystem][component]:
                # worksheet.write(raw_num, 2, bundle_path)
                error_rules = all_error_json[subsystem][component][bundle_path]
                for rule in error_rules:
                    # worksheet.write(raw_num, 3, rule)
                    error_list = error_rules[rule]
                    for bundle_errors in error_list:
                        # print(bundle_errors) # test
                        raw_begin = raw_num # 同一个bundle 的起始行
                        for key in bundle_errors:
                            if type(bundle_errors[key]) == dict:
                                for filed_name in bundle_errors[key]:
                                    worksheet.write(raw_num, 4, key + ': ' + filed_name)
                                    try:
                                        worksheet.write(raw_num, 5, str(bundle_errors[key][filed_name]))
                                    except:
                                        print(bundle_path)
                                    raw_num += 1
                            else:
                                worksheet.write(raw_num, 4, key)
                                worksheet.write(raw_num, 5, bundle_errors[key])
                                raw_num += 1
                        raw_end = raw_num - 1 # 同一个bundle 的末尾行
                        # 合并同样的 bundle 路径列
                        # 设置单元格样式为居中
                        style = xlwt.XFStyle()
                        # 对齐设置
                        ali = xlwt.Alignment()
                        ali.vert = 0x01 # 垂直对齐方式为居中
                        style.alignment = ali
                        worksheet.write_merge(raw_begin, raw_end, 0, 0, subsystem, style)
                        worksheet.write_merge(raw_begin, raw_end, 1, 1, component, style)
                        worksheet.write_merge(raw_begin, raw_end, 2, 2, bundle_path, style)
                        worksheet.write_merge(raw_begin, raw_end, 3, 3, rule, style)
                        # workbook.save('./test/测试.xls') # test
                        # exit(0) # test
    out_path = os.path.normpath(output_path) + '/' + 'all_bundle_error.xls'
    workbook.save(out_path)
    print("Please check " + out_path)

def export_to_json(all_errors:dict, output_path:str):
    all_error_json = json.dumps(all_errors, indent=4, separators=(', ', ': '))
    out_path = os.path.normpath(output_path) + '/' + 'all_bundle_error.json'
    with open(out_path, 'w') as f:
        f.write(all_error_json)
    print("Please check " + out_path)

def check_all_bundle_json(project_path) -> dict:
    script_path = os.getcwd()
    os.chdir(project_path)
    all_bundle = get_all_bundle_json()
    all_error = {}
    for bundle_json_path in all_bundle:
        bundle_path = bundle_json_path.strip('\n')
        bundle = Bundle(bundle_path)
        bundle_error = bundle.check()
        
        subsystem_name = bundle.subsystem_name()
        component_name = bundle.component_name()
        if len(subsystem_name) == 0:
            subsystem_name = "Unknow"
        if len(component_name) == 0:
            component_name = "Unknow"
        if subsystem_name not in all_error:
            all_error[subsystem_name] = {}
        if component_name not in all_error[subsystem_name]:
            all_error[subsystem_name][component_name] = {}
        component_error = all_error[subsystem_name][component_name]

        if bundle_error:
            component_error[bundle_path] = {}
            component_error[bundle_path][CHECK_RULE_2_1] = []
            component_error[bundle_path][CHECK_RULE_2_1].append(bundle_error)

    os.chdir(script_path)
    return all_error

def is_project(path) -> bool:
    p = os.path.normpath(path)
    return os.path.exists(p + '/' + '.repo/manifests')

def parse_args():
    parser = argparse.ArgumentParser()
    # exclusive output format
    ex_format = parser.add_mutually_exclusive_group()
    ex_format.add_argument("--xls", help="output format: xls(default).",
                           action="store_true")
    ex_format.add_argument("--json", help="output format: json.",
                           action="store_true")
    # exclusive input
    ex_input = parser.add_mutually_exclusive_group()
    ex_input.add_argument("-P", "--project", help="project root path.", type=str)
    ex_input.add_argument("-p", "--path", help="bundle.json path list.", nargs='+')
    args = parser.parse_args()
    if args.project:
        ohos_path = args.project
        if is_project(ohos_path):
            all_errors = check_all_bundle_json(ohos_path)
            if args.json:
                export_to_json(all_errors, '.')
            else:
                export_to_xls(all_errors, '.')
        else:
            print("'" + ohos_path + "' is not a oopeharmony project.")
            exit(1)
    elif args.path:
        bundle_list_error = {}
        
        for bundle_json_path in args.path:
            bundle = Bundle(bundle_json_path)
            error_field = bundle.check()
            if error_field:
                # bundle_list_error[bundle_json_path] = error_field
                bundle_list_error[bundle_json_path] = {}
                bundle_list_error[bundle_json_path][CHECK_RULE_2_1] = []
                bundle_list_error[bundle_json_path][CHECK_RULE_2_1].append(error_field)
        # test
        test_json = json.dumps(bundle_list_error,
                               indent=4, separators=(', ', ': '), 
                               ensure_ascii=False)
        print(test_json)
    else:
        print("use '-h' get help.")

class Bundle(object):
    def __init__(self, path:str) -> None:
        # self.__path = path
        self.__all_errors = {}
        with open(path, 'r') as f:
            try:
                self.__json = json.load(f)
            except json.decoder.JSONDecodeError as e:
                print("'" + path + "'" + " is not a bundle.json file.")
                exit(1)

    def component_name(self) -> str:
        return self.__json["component"]["name"]

    def subsystem_name(self) -> str: # 目前存在为空
        return self.__json["component"]["subsystem"]

    def check(self) -> dict:
        self.check_name()
        self.check_version()
        self.check_segment()
        self.check_component()
        return self.__all_errors
    # name
    def check_name(self):
        if 'name' not in self.__json:
            self.__all_errors['name'] = NO_FIELD
            return 
        name = self.__json['name']
        if not name: # 为空
            self.__all_errors['name'] = ERROR_EMPTY
            return
        if '@ohos/' != name[:6]:
            self.__all_errors['name'] = NAME_FORMAT_ERROR
            return
        if len(name.split('/')[1]) < 1:
            self.__all_errors['name'] = NAME_FORMAT_ERROR
            return
        if len(name.split('/')[1]) > 63:
            self.__all_errors['name'] = COMPONENT_NAME_TOO_LONG
            return
        if not name.islower():
            self.__all_errors['name'] = NAME_NOT_LOWER
            return
    # version
    def check_version(self):
        if 'version' not in self.__json:
            self.__all_errors['version'] = NO_FIELD
            return
        if len(self.__json['version']) < 3: # 3.1
            self.__all_errors['version'] = VERSION_ERROR
            return
    # segment
    def check_segment(self):
        if 'segment' not in self.__json:
            self.__all_errors['segment'] = NO_FIELD
            return
        elif 'destPath' not in self.__json['segment']:
            self.__all_errors['segment'] = {}
            self.__all_errors['segment']['destPath'] = NO_FIELD
            return
        self.__all_errors['segment'] = {}
        path = self.__json['segment']['destPath']
        if not path:
            self.__all_errors['segment']['destPath'] = ERROR_EMPTY
            return
        if type(path) != str:
            self.__all_errors['segment']['destPath'] = ONLY_ONE_DESTPATH
            return
        if not self.__all_errors['segment']:
            self.__all_errors.pop('segment')
    # component
    def check_component(self):
        if 'component' not in self.__json:
            self.__all_errors['component'] = NO_FIELD
            return
        component = self.__json['component']
        self.__all_errors['component'] = {}
        component_error = self.__all_errors['component']
        
        # name
        if 'name' not in component:
            component_error['name'] = NO_FIELD
        else:
            if not component['name']:
                component_error['name'] = ERROR_EMPTY
            elif 'name' in self.__json and '/' in self.__json['name']:
                if component['name'] != self.__json['name'].split('/')[1]:
                    component_error['name'] = COMPONENT_NAME_ERROR
        
        # subsystem
        if 'subsystem' not in component:
            component_error['subsystem'] = NO_FIELD
        else:
            if not is_all_lower(component['subsystem']):
                component_error['subsystem'] = SUBSYSTEM_MUST_LOWER
        
        # syscap
        if 'syscap' not in component:
            component_error['syscap'] = NO_FIELD
        else:
            syscap_error_list = []
            for i in component['syscap']:
                err = {}
                if not i: # syscap empty
                    err[i] = 'syscap string ' + ERROR_EMPTY
                    syscap_error_list.append(err)
                    continue
                # check every syscap
                words = i.split('.')
                err[i] = [] # one syscap's error list
                if words[0] != 'SystemCapability':
                    err[i].append(SYSCAP_HEADER_ERROR)
                for j in words:
                    if not j[0].isupper():
                        err[i].append(PASCAL_CASING_STYLE)
                if len(words) < 3: # SystemCapability.Subsystem.Component[.Subcomponent]
                    err[i].append(SYSCAP_FORMAT_ERROR)
                if err[i]:
                    syscap_error_list.append(err)
            if syscap_error_list:
                component_error['syscap'] = syscap_error_list
        
        # feature
        if 'features' not in component:
            component_error['feature'] = NO_FIELD
        elif not component["features"]:
            pass
        else:
            features_error_list = []
            for ft in component["features"]:
                err = {}
                err[ft] = []
                if not '_' in ft:
                    err[ft].append(FEATURE_SEPARATOR_ERROR)
                    features_error_list.append(err)
                    continue
                if not ft.startswith(component["name"]):
                    err[ft].append(FEATURE_PREFIX_ERROR)
                if err[ft]:
                    features_error_list.append(err)
        
        # adapted_system_type
        if 'adapted_system_type' not in component:
            component_error["adapted_system_type"] = NO_FIELD
        else:
            ast = component["adapted_system_type"]
            if not ast:
                component_error["adapted_system_type"] = ERROR_EMPTY
            type_set = tuple(set(ast))
            if len(type_set) != len(ast):
                return "'adapted_system_type' can not have duplicate type."
            if len(ast) > 3:
                return "'adapted_system_type' only 3 at most(mini,small,standard)"
            all_type_list = ["mini", "small", "standard"]
            error_type = []
            for i in ast:
                if i not in all_type_list:
                    error_type.append(i)
            if error_type:
                component_error["adapted_system_type"] = str(error_type) + " not the correct type."
        
        # rom
        if 'rom' not in component:
            component_error['rom'] = NO_FIELD
        elif not component["rom"]:
                component_error['rom'] = ERROR_EMPTY
        elif component["rom"][0] == '-':
            component_error['rom'] = "size must greater than 0."
            num, unit = format_str_value(component["rom"])
            rom_error = ""
            if num <= 0:
                rom_error += 'must be grater than 0. '
            unit_list = ['', "KB", "KByte", "MByte", "MB"]
            if unit not in unit_list:
                rom_error += "And unit error."
            if rom_error:
                component["rom"] = rom_error
            
        # ram
        if 'ram' not in component:
            component_error['ram'] = NO_FIELD
        elif not component["ram"]:
                component_error['ram'] = ERROR_EMPTY
        elif component["ram"][0] == '-':
            component_error['ram'] = "size must greater than 0."
            num, unit = format_str_value(component["ram"])
            ram_error = ""
            if num <= 0:
                ram_error += 'must be grater than 0. '
            unit_list = ['', "KB", "KByte", "MByte", "MB"]
            if unit not in unit_list:
                ram_error += "And unit error."
            if ram_error:
                component["ram"] = ram_error

        # deps
        if 'deps' not in component:
            component_error['deps'] = NO_FIELD
        else:
            pass
        
        if not component_error:
            self.__all_errors.pop('component')


if __name__ == '__main__':
    parse_args()
